6.8. Хозяин файла, процесса, и проверка привелегий.
UNIX - многопользовательская система. Это значит, что одновременно на разных терминалах, подключенных к машине, могут работать разные пользователи (а может и один на нескольких терминалах). На каждом терминале работает свой интерпретатор команд, являющийся потомком процесса /etc/init.

6.8.1. Теперь - про функции, позволяющие узнать некоторые данные про любого пользователя системы. Каждый пользователь в UNIX имеет уникальный номер: идентификатор пользователя (user id), а также уникальное имя: регистрационное имя, которое он набирает для входа в систему. Вся информация о пользователях хранится в файле /etc/passwd. Существуют функции, позволяющие по номеру пользователя узнать регистрационное имя и наоборот, а заодно получить еще некоторую информацию из passwd:

    #include <stdio.h>
    #include <pwd.h>
    struct passwd *p;
    int   uid;   /* номер */
    char *uname; /* рег. имя */

    uid = getuid();
    p   = getpwuid( uid   );
            ...
    p   = getpwnam( uname );
Эти функции возвращают указатели на статические структуры, скрытые внутри этих функций. Структуры эти имеют поля:

    p->pw_uid     идентиф. пользователя (int uid);
    p->pw_gid     идентиф. группы пользователя;

            и ряд полей типа char[]
    p->pw_name    регистрационное имя пользователя (uname);
    p->pw_dir     полное имя домашнего каталога
      (каталога, становящегося текущим при входе в систему);
    p->pw_shell   интерпретатор команд
      (если "", то имеется в виду /bin/sh);
    p->pw_comment произвольная учетная информация (не используется);
    p->pw_gecos   произвольная учетная информация (обычно ФИО);
    p->pw_passwd  зашифрованный пароль для входа в
       систему. Истинный пароль нигде не хранится вовсе!
Функции возвращают значение p==NULL, если указанный пользователь не существует (например, если задан неверный uid). uid хозяина данного процесса можно узнать вызовом getuid, а uid владельца файла - из поля st_uid структуры, заполняемой системным вызовом stat (а идентификатор группы владельца - из поля st_gid). Задание: модифицируйте наш аналог программы ls, чтобы он выдавал в текстовом виде имя владельца каждого файла в каталоге.

6.8.2. Владелец файла может изменить своему файлу идентификаторы владельца и группы вызовом

    chown(char *имяФайла, int uid, int gid);
т.е. "подарить" файл другому пользователю. Забрать чужой файл себе невозможно. При этой операции биты S_ISUID и S_ISGID в кодах доступа к файлу (см. ниже) сбрасываются, поэтому создать "Троянского коня" и, сделав его хозяином суперпользователя, получить неограниченные привелегии - не удастся!

6.8.3. Каждый файл имеет своего владельца (поле di_uid в I-узле на диске или поле i_uid в копии I-узла в памяти ядра А. Богатырев, 1992-95 Си в UNIX*). Каждый процесс также имеет своего владельца (поля u_uid и u_ruid в u-area). Как мы видим, процесс имеет два параметра, обозначающие владельца. Поле ruid называется "реальным идентификатором" пользователя, а uid "эффективным идентификатором". При вызове exec() заменяется программа, выполняемая данным процессом:

     старая программа  exec    новая программа
         ruid -->----------------->---> ruid
         uid  -->--------*-------->---> uid (new)
                         |
                    выполняемый файл
                     i_uid (st_uid)
Как видно из этой схемы, реальный идентификатор хозяина процесса наследуется. Эффективный идентификатор обычно также наследуется, за исключением одного случая: если в кодах доступа файла (i_mode) выставлен бит S_ISUID (set-uid bit), то значение поля u_uid в новом процессе станет равно значению i_uid файла с программой:

    /* ... во время exec ... */
    p_suid = u_uid;     /* спасти */
    if( i_mode & S_ISUID ) u_uid = i_uid;
    if( i_mode & S_ISGID ) u_gid = i_gid;
т.е. эффективным владельцем процесса станет владелец файла. Здесь gid - это идентификаторы группы владельца (которые тоже есть и у файла и у процесса, причем у процесса - реальный и эффективный).

Зачем все это надо? Во-первых затем, что ПРАВА процесса на доступ к какому-либо файлу проверяются именно для эффективного владельца процесса. Т.е. например, если файл имеет коды доступа

    mode = i_mode & 0777;
                  /* rwx rwx rwx */
и владельца i_uid, то процесс, пытающийся открыть этот файл, будет "проэкзаменован" в таком порядке:

    if( u_uid == 0 )  /* super user */
         то доступ разрешен;
    else if( u_uid == i_uid )
         проверить коды (mode & 0700);
    else if( u_gid == i_gid )
         проверить коды (mode & 0070);
    else проверить коды (mode & 0007);
Процесс может узнать свои параметры:

    unsigned short uid  = geteuid();  /* u_uid  */
    unsigned short ruid = getuid();   /* u_ruid */
    unsigned short gid  = getegid();  /* u_gid  */
    unsigned short rgid = getuid();   /* u_rgid */
а также установить их:

    setuid(newuid);  setgid(newgid);
Рассмотрим вызов setuid. Он работает так (u_uid - относится к процессу, издавшему этот вызов):

    if(      u_uid == 0 /* superuser */ )
             u_uid = u_ruid =    p_suid =  newuid;
    else if( u_ruid == newuid || p_suid == newuid )
             u_uid = newuid;
    else     неудача;
Поле p_suid позволяет set-uid-ной программе восстановить эффективного владельца, который был у нее до exec-а.

Во-вторых, все это надо для следующего случая: пусть у меня есть некоторый файл BASE с хранящимися в нем секретными сведениями. Я являюсь владельцем этого файла и устанавливаю ему коды доступа 0600 (чтение и запись разрешены только мне). Тем не менее, я хочу дать другим пользователям возможность работать с этим файлом, однако контролируя их деятельность. Для этого я пишу программу, которая выполняет некоторые действия с файлом BASE, при этом проверяя законность этих действий, т.е. позволяя делать не все что попало, а лишь то, что я в ней предусмотрел, и под жестким контролем. Владельцем файла PROG, в котором хранится эта программа, также являюсь я, и я задаю этому файлу коды доступа 0711 (rwx--x--x) - всем можно выполнять эту программу. Все ли я сделал, чтобы позволить другим пользоваться базой BASE через программу (и только нее) PROG? Нет!

Если кто-то другой запустит программу PROG, то эффективный идентификатор процесса будет равен идентификатору этого другого пользователя, и программа не сможет открыть мой файл BASE. Чтобы все работало, процесс, выполняющий программу PROG, должен работать как бы от моего имени. Для этого я должен вызовом chmod либо командой

     chmod u+s PROG
добавить к кодам доступа файла PROG бит S_ISUID.

После этого, при запуске программы PROG, она будет получать эффективный идентификатор, равный моему идентификатору, и таким образом сможет открыть и работать с файлом BASE. Вызов getuid позволяет выяснить, кто вызвал мою программу (и занести это в протокол, если надо).

Программы такого типа - не редкость в UNIX, если владельцем программы (файла ее содержащего) является суперпользователь. В таком случае программа, имеющая бит доступа S_ISUID работает от имени суперпользователя и может выполнять некоторые действия, запрещенные обычным пользователям. При этом программа внутри себя делает всяческие проверки и периодически спрашивает пароли, то есть при работе защищает систему от дураков и преднамеренных вредителей. Простейшим примером служит команда ps, которая считывает таблицу процессов из памяти ядра и распечатывает ее. Доступ к физической памяти машины производится через файл-псевдоустройство /dev/mem, а к памяти ядра /dev/kmem. Чтение и запись в них позволены только суперпользователю, поэтому программы "общего пользования", обращающиеся к этим файлам, должны иметь бит set-uid.

Откуда же изначально берутся значения uid и ruid (а также gid и rgid) у процесса? Они берутся из процесса регистрации пользователя в системе: /etc/getty. Этот процесс запускается на каждом терминале как процесс, принадлежащий суперпользователю (u_uid==0). Сначала он запрашивает имя и пароль пользователя:

    #include <stdio.h>  /* cc -lc_s */
    #include <pwd.h>
    #include <signal.h>
    struct passwd *p;
    char userName[80], *pass, *crpass;
    extern char *getpass(), *crypt();
      ...
    /* Не прерываться по сигналам с клавиатуры */
    signal (SIGINT, SIG_IGN);
    for(;;){
      /* Запросить имя пользователя: */
      printf("Login: "); gets(userName);
      /* Запросить пароль (без эха): */
      pass = getpass("Password: ");
      /* Проверить имя: */
      if(p = getpwnam(userName)){
         /* есть такой пользователь */
         crpass = (p->pw_passwd[0]) ? /* если есть пароль */
                  crypt(pass, p->pw_passwd) : pass;
         if( !strcmp( crpass, p->pw_passwd))
                  break; /* верный пароль */
      }
      printf("Login incorrect.\a\n");
    }
    signal (SIGINT, SIG_DFL);
Затем он выполняет:

    // ... запись информации о входе пользователя в систему
    // в файлы /etc/utmp (кто работает в системе сейчас)
    // и       /etc/wtmp (список всех входов в систему)
            ...
    setuid( p->pw_uid ); setgid( p->pw_gid );
    chdir ( p->pw_dir ); /* GO HOME! */
    // эти параметры будут унаследованы
    // интерпретатором команд.
            ...
    // настройка некоторых переменных окружения envp:
    // HOME     = p->pw_dir
    // SHELL    = p->pw_shell
    // PATH     = нечто по умолчанию, вроде :/bin:/usr/bin
    // LOGNAME (USER) = p->pw_name
    // TERM     = считывается из файла
    //            /etc/ttytype по имени устройства av[1]
    // Делается это как-то подобно
    //   char *envp[MAXENV], buffer[512]; int envc = 0;
    //   ...
    //   sprintf(buffer, "HOME=%s", p->pw_dir);
    //   envp[envc++] = strdup(buffer);
    //   ...
    //   envp[envc] = NULL;
            ...
    // настройка кодов доступа к терминалу. Имя устройства
    // содержится в параметре av[1] функции main.
    chown (av[1], p->pw_uid, p->pw_gid);
    chmod (av[1], 0600 );  /* -rw------- */
    // теперь доступ к данному терминалу имеют только
    // вошедший в систему пользователь и суперпользователь.
    // В случае смерти интерпретатора команд,
    // которым заменится getty, процесс init сойдет
    // с системного вызова ожидания wait() и выполнит
    //  chown ( этот_терминал, 2 /*bin*/, 15 /*terminal*/ );
    //  chmod ( этот_терминал, 0600 );
    // и, если терминал числится в файле описания линий
    // связи /etc/inittab как активный (метка respawn), то
    // init перезапустит на этом_терминале новый
    // процесс getty при помощи пары вызовов fork() и exec().
            ...
    // запуск интерпретатора команд:
    execle( *p->pw_shell ? p->pw_shell : "/bin/sh",
                      "-", NULL, envp );
В результате он становится процессом пользователя, вошедшего в систему. Таковым же после exec-а, выполняемого getty, остается и интерпретатор команд p->pw_shell (обычно /bin/sh или /bin/csh) и все его потомки.

На самом деле, в описании регистрации пользователя при входе в систему, сознательно было допущено упрощение. Дело в том, что все то, что мы приписали процессу getty, в действительности выполняется двумя программами: /etc/getty и /bin/login.

Сначала процесс getty занимается настройкой параметров линии связи (т.е. терминала) в соответствии с ее описанием в файле /etc/gettydefs. Затем он запрашивает имя пользователя и заменяет себя (при помощи сисвызова exec) процессом login, передавая ему в качестве одного из аргументов полученное имя пользователя.

Затем login запрашивает пароль, настраивает окружение, и.т.п., то есть именно он производит все операции, приведенные выше на схеме. В конце концов он заменяет себя интерпретатором команд.

Такое разделение делается, в частности, для того, чтобы считанный пароль в случае опечатки не хранился бы в памяти процесса getty, а уничтожался бы при очистке памяти завершившегося процесса login. Таким образом пароль в истинном, незашифрованном виде хранится в системе минимальное время, что затрудняет его подсматривание средствами электронного или программного шпионажа. Кроме того, это позволяет изменять систему проверки паролей не изменяя программу инициализации терминала getty.

Имя, под которым пользователь вошел в систему на данном терминале, можно узнать вызовом стандартной функции

     char *getlogin();
Эта функция не проверяет uid процесса, а просто извлекает запись про данный терминал из файла /etc/utmp.

Наконец отметим, что владелец файла устанавливается при создании этого файла (вызовами creat или mknod), и полагается равным эффективному идентификатору создающего процесса.

    di_uid = u_uid;     di_gid = u_gid;
6.8.4. Напишите программу, узнающую у системы и распечатывающую: номер процесса, номер и имя своего владельца, номер группы, название и тип терминала на котором она работает (из переменной окружения TERM).

* - В некотором буфере запоминается текущее состояние процесса: положение вершины стека вызовов функций (stack pointer); состояние всех регистров процессора, включая регистр адреса текущей машинной команды (instruction pointer).

* - Это достигается восстановлением состояния процесса из буфера. Изменения, происшедшие за время между setjmp и longjmp в статических данных не отменяются (т.к. они не сохранялись).

* - При открытии файла и вообще при любой операции с файлом, в таблицах ядра заводится копия I-узла (для ускорения доступа, чтобы постоянно не обращаться к диску). Если I-узел в памяти будет изменен, то при закрытии файла (а также периодически через некоторые промежутки времени) эта копия будет записана обратно на диск. Структура I-узла в памяти - struct inode - описана в файле <sys/inode.h>, а на диске - struct dinode - в файле <sys/ino.h>.